import { FileSystem } from '@/source/storage/file-system';
import { basename, dirname, joinPath, slash, splitPath } from '@/source/path';
import type { ResolvedLoaderConfig } from '../loader';
import type { SourceConfig } from '../source';

export type ContentStorage<Config extends SourceConfig = SourceConfig> =
  FileSystem<ContentStorageFile<Config>>;

export type ContentStorageFile<Config extends SourceConfig = SourceConfig> =
  | {
      path: string;
      absolutePath?: string;

      format: 'meta';
      data: Config['metaData'];
    }
  | {
      path: string;
      absolutePath?: string;

      format: 'page';
      slugs: string[];
      data: Config['pageData'];
    };

function isLocaleValid(locale: string) {
  return locale.length > 0 && !/\d+/.test(locale);
}

const parsers = {
  dir(path: string): [string, string?] {
    const [locale, ...segs] = path.split('/');

    if (locale && segs.length > 0 && isLocaleValid(locale))
      return [segs.join('/'), locale];

    return [path];
  },
  dot(path: string): [string, string?] {
    const dir = dirname(path);
    const base = basename(path);
    const parts = base.split('.');
    if (parts.length < 3) return [path];

    const [locale] = parts.splice(parts.length - 2, 1);
    if (!isLocaleValid(locale)) return [path];

    return [joinPath(dir, parts.join('.')), locale];
  },
  none(path: string): [string, string?] {
    return [path];
  },
};

/**
 * @param defaultLanguage - language to use when i18n is not configured.
 * @returns a map of locale and its content storage.
 *
 * in the storage, locale codes are removed from file paths, hence the same file will have same file paths in every storage.
 */
export function buildContentStorage(
  loaderConfig: ResolvedLoaderConfig,
  defaultLanguage: string,
): Record<string, ContentStorage> {
  const {
    source,
    plugins = [],
    i18n = {
      defaultLanguage,
      parser: 'none',
      languages: [defaultLanguage],
    },
  } = loaderConfig;

  const parser = parsers[i18n.parser ?? 'dot'];
  const storages: Record<string, ContentStorage> = {};
  const normalized = new Map<
    string,
    {
      pathWithoutLocale: string;
      file: ContentStorageFile;
    }[]
  >();

  for (const inputFile of source.files) {
    let file: ContentStorageFile;
    if (inputFile.type === 'page') {
      file = {
        format: 'page',
        path: normalizePath(inputFile.path),
        // will be generated by the slugs plugin if unspecified
        slugs: inputFile.slugs as string[],
        data: inputFile.data,
        absolutePath: inputFile.absolutePath,
      };
    } else {
      file = {
        format: 'meta',
        path: normalizePath(inputFile.path),
        absolutePath: inputFile.absolutePath,
        data: inputFile.data,
      };
    }

    const [pathWithoutLocale, locale = i18n.defaultLanguage] = parser(
      file.path,
    );
    const list = normalized.get(locale) ?? [];
    list.push({
      pathWithoutLocale,
      file,
    });
    normalized.set(locale, list);
  }

  const fallbackLang =
    i18n.fallbackLanguage !== null
      ? (i18n.fallbackLanguage ?? i18n.defaultLanguage)
      : null;

  function scan(lang: string) {
    if (storages[lang]) return;

    let storage: ContentStorage;
    if (fallbackLang && fallbackLang !== lang) {
      scan(fallbackLang);
      storage = new FileSystem(storages[fallbackLang]);
    } else {
      storage = new FileSystem();
    }

    for (const { pathWithoutLocale, file } of normalized.get(lang) ?? []) {
      storage.write(pathWithoutLocale, file);
    }

    const context = {
      storage,
    };
    for (const plugin of plugins) {
      plugin.transformStorage?.(context);
    }

    storages[lang] = storage;
  }

  for (const lang of i18n.languages) scan(lang);
  return storages;
}

/**
 * @param path - Relative path
 * @returns Normalized path, with no trailing/leading slashes
 * @throws Throws error if path starts with `./` or `../`
 */
function normalizePath(path: string): string {
  const segments = splitPath(slash(path));
  if (segments[0] === '.' || segments[0] === '..')
    throw new Error("It must not start with './' or '../'");
  return segments.join('/');
}
